/*************************************************************************
* (c) Copyright 2017 Hewlett Packard Enterprise Development Company LP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
************************************************************************/
package com.eucalyptus.ws.handlers;
import java.io.InputStream;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import com.amazonaws.ReadLimitInfo;
import com.amazonaws.SignableRequest;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.http.HttpMethodName;
import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.principal.AccountIdentifiers;
import com.eucalyptus.auth.principal.User;
import com.eucalyptus.auth.tokens.SecurityTokenAWSCredentialsProvider;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.LockResource;
import com.eucalyptus.util.Pair;
import com.eucalyptus.ws.IoMessage;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import io.netty.buffer.ByteBufInputStream;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders;
/**
* Outbound hmac signature v4 for internal use.
*
* If we switch to netty 4.x for inbound then this could extend HmacHandler /
* IoHmacHandler and also cover inbound.
*/
@Sharable
public class IoInternalHmacHandler extends ChannelOutboundHandlerAdapter {
private static final long SESSION_DURATION = TimeUnit.MINUTES.toMillis( 15 );
private static final long PRE_EXPIRY = TimeUnit.MINUTES.toMillis( 5 );
private static final long EXPIRY_OFFSET = TimeUnit.MINUTES.toMillis( 1 );
private final Lock credentialsRefreshLock = new ReentrantLock( );
private final AtomicReference<Pair<Long, AWSCredentials>> credentialsRef = new AtomicReference<>( );
private final Supplier<User> systemUserSupplier =
Suppliers.memoizeWithExpiration( systemUserSupplier( ), 1, TimeUnit.MINUTES );
private final SecurityTokenAWSCredentialsProvider provider =
new SecurityTokenAWSCredentialsProvider( systemUserSupplier, (int)SESSION_DURATION/1000, 0 );
@Override
public void write(
final ChannelHandlerContext ctx,
final Object msg,
final ChannelPromise promise
) throws Exception {
if ( msg instanceof IoMessage && ((IoMessage)msg).isRequest( ) ) {
final IoMessage ioMessage = (IoMessage) msg;
final FullHttpRequest httpRequest = (FullHttpRequest) ioMessage.getHttpMessage( );
sign( httpRequest );
}
super.write( ctx, msg, promise );
}
private void sign( final FullHttpRequest request ) {
final AWS4Signer signer = new AWS4Signer( );
signer.setRegionName( "eucalyptus" );
signer.setServiceName( "internal" );
signer.sign( wrapRequest( request ), credentials( ) );
}
private SignableRequest<?> wrapRequest( final FullHttpRequest request ) {
return new SignableRequest( ) {
@Override
public Map<String, String> getHeaders( ) {
return request.headers( ).entries( ).stream( )
.collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) );
}
@Override
public String getResourcePath( ) {
return request.getUri( );
}
@Override
public Map<String, List<String>> getParameters( ) {
return Collections.emptyMap( );
}
@Override
public URI getEndpoint( ) {
return URI.create( "http://" + request.headers().get( HttpHeaders.Names.HOST ) );
}
@Override
public HttpMethodName getHttpMethod( ) {
return HttpMethodName.fromValue( request.getMethod( ).name( ) );
}
@Override
public int getTimeOffset( ) {
return 0;
}
@Override
public InputStream getContent( ) {
return new ByteBufInputStream( request.content( ).slice( ) );
}
@Override
public InputStream getContentUnwrapped( ) {
return getContent( );
}
@Override
public ReadLimitInfo getReadLimitInfo( ) {
return ( ) -> request.content( ).readableBytes( );
}
@Override
public Object getOriginalRequestObject( ) {
throw new RuntimeException( "Not supported" );
}
@Override
public void addHeader( final String s, final String s1 ) {
request.headers( ).set( s, s1 );
}
@Override
public void addParameter( final String s, final String s1 ) {
throw new RuntimeException( "Not supported" );
}
@Override
public void setContent( final InputStream inputStream ) {
throw new RuntimeException( "Not supported" );
}
};
}
private AWSCredentials credentials() {
final Pair<Long, AWSCredentials> credentialsPair = credentialsRef.get( );
if ( credentialsPair == null || credentialsPair.getLeft( ) < expiry(EXPIRY_OFFSET) ) {
// no credentials or they have expired, must wait for new
try ( final LockResource lock = LockResource.lock( credentialsRefreshLock ) ) {
return perhapsRefreshCredentials( );
}
} else if ( credentialsPair.getLeft( ) < expiry(PRE_EXPIRY) ) {
// credentials pre-expired, refresh if no one else is doing so
try ( final LockResource lock = LockResource.tryLock( credentialsRefreshLock ) ) {
if ( lock.isLocked( ) ) {
return perhapsRefreshCredentials( );
} else {
return credentialsPair.getRight( );
}
}
} else {
return credentialsPair.getRight( );
}
}
/**
* Caller must hold credentials lock.
*/
private AWSCredentials perhapsRefreshCredentials() {
final Pair<Long, AWSCredentials> credentialsPair = credentialsRef.get( );
if ( credentialsPair != null && credentialsPair.getLeft( ) > expiry(EXPIRY_OFFSET) ) {
return credentialsPair.getRight( );
} else {
provider.refresh( );
final Pair<Long, AWSCredentials> newCredentialsPair =
Pair.of( System.currentTimeMillis( ) + SESSION_DURATION, provider.getCredentials( ) );
credentialsRef.set( newCredentialsPair );
return newCredentialsPair.getRight( );
}
}
private long expiry( final long offset ) {
return System.currentTimeMillis( ) + offset;
}
private Supplier<User> systemUserSupplier( ) {
return ( ) -> {
try {
return Accounts.lookupSystemAccountByAlias( AccountIdentifiers.SYSTEM_ACCOUNT );
} catch ( final Exception e ) {
throw Exceptions.toUndeclared( e );
}
};
}
}